package com.myframework.utils.jsutil;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

public class JavascriptUtil {

	/**
	 * Serialize any object of the following types to a Javascript usable form.
	 * <ul>
	 * <li><code>null</code></li>
	 * <li><code>Map</code>&Dagger;</li>
	 * <li><code>Object[]</code>&Dagger;</li>
	 * <li><code>List</code>&Dagger;</li>
	 * <li><code>Date</code></li>
	 * <li><code>Number</code></li>
	 * <li><code>String</code></li>
	 * </ul>
	 * &Dagger;<i>The objects contained in these container objects should be
	 * of the types listed above.</i>
	 *
	 * @param object Object to serialize
	 * @return A string representation of the serialized object
	 */
	@SuppressWarnings("rawtypes")
	public static String serializeToJS(Object object) {
		return
				(null == object ? "null" :
					(object instanceof Date ? toJSDate((Date) object) :
						(object instanceof Map ? toJSObject((Map) object) :
							(object instanceof Object[] ? toJSArray((Object[]) object) :
								(object instanceof List ? toJSArray((List) object) :
									(object instanceof Number || object instanceof Boolean ? object.toString() :
										"\"" + escapeJavaScript(object.toString()) + "\""))))));
	}

	/**
	 * Using no escape JavaScipt mode
	 */
	@SuppressWarnings("rawtypes")
	public static String serializeToJSNoEsc(Object object) {
		return
				(null == object ? "null" :
					(object instanceof Date ? toJSDate((Date) object) :
						(object instanceof Map ? toJSObject((Map) object) :
							(object instanceof Object[] ? toJSArray((Object[]) object) :
								(object instanceof List ? toJSArray((List) object) :
									(object instanceof Number || object instanceof Boolean ? object.toString() :
										"\"" + object.toString() + "\""))))));
	}

	/**
	 * Convert a Java {@link Date} to a JavaScript formatted date string.
	 * The output format is "MM/dd/yyyy" for the current locale. If <tt>dt</tt> is
	 * <tt>null</tt> an empty string will be returned.
	 *
	 * @param dt The date to format
	 * @return A Date formatted to a String representing the equivalent Javascript Date
	 */
	public static String toJSDate(Date dt) {
		return (dt != null ? toJSDate(dt, null) : "");
	}

	/**
	 * Convert a Java {@link Date} to a JavaScript formatted date string.
	 * The output format is "MM/dd/yyyy" for the current locale. If <tt>dt</tt> is
	 * <tt>null</tt> an empty string will be returned.
	 *
	 * @param dt	  The date to format
	 * @param format The format string.  If <code>null</code>, defaults to "MM/dd/yyyy".
	 * @return A Date formatted to a String representing the equivalent Javascript Date
	 */
	public static String toJSDate(Date dt, String format) {
		if (dt == null) {
			return "";
		}

		if (format == null || "".equals(format)) {
			format = "MM/dd/yyyy";
		}

		SimpleDateFormat formatter = new SimpleDateFormat(format);
		return "\"" + formatter.format(dt) + "\"";
	}

	/**
	 * Convert a Map to a Javascript object, mapping keys and values to
	 * fields and values.  The output <i>cannot</i> be evaluated as an object on the
	 * client, but passes JSON validation and works with Douglas Crockford's
	 * JSON decoder.  The key and value will be serialized via the {@link #serializeToJS}
	 * method.  Use care to make sure that your keys are not <code>null</code>.
	 * <p/>
	 * If you need to have the object pass <tt>eval()</tt> on the client, see
	 * the {@link #toJSEvalObject} method instead.  However, be aware that
	 * using that method opens up potential risks of XSS if the data is unchecked.
	 *
	 * @param inMap A Map
	 * @return A String representing a Javascript object
	 * @see #serializeToJS
	 * @see #toJSEvalObject
	 */
	@SuppressWarnings("rawtypes")
	public static String toJSObject(Map inMap) {
		String outVal = "";
		if (null != inMap) {
			for (Iterator it = inMap.keySet().iterator(); it.hasNext();) {
				Object key = it.next();
				outVal += (0 != outVal.length() ? "," : "") + serializeToJS(key) + ":" + serializeToJS(inMap.get(key));
			}
		}

		return "{" + outVal + "}";
	}

	/**
	 * Convert an array of Java objects into an array for use in JavaScript.
	 * The array will be surrounded by brackets.  The values are serialized
	 * using the {@link #serializeToJS} method.
	 *
	 * @param inArray An array of values.
	 * @return A String representing a Javascript array
	 * @see #serializeToJS
	 */
	public static String toJSArray(Object[] inArray) {
		String outVal = "";
		if (null != inArray) {
			for (int i = 0; i < inArray.length; i++) {
				outVal += (i > 0 ? "," : "") + serializeToJS(inArray[i]);
			}
		}

		return "[" + outVal + "]";
	}

	/**
	 * Convert a List of Java objects into an array for use in JavaScript.
	 * The array will be surrounded by brackets.  The values are serialized via
	 * the {@link #serializeToJS} method.
	 *
	 * @param inList A List of values.
	 * @return A String representing a Javascript array
	 * @see #serializeToJS
	 */
	@SuppressWarnings("rawtypes")
	public static String toJSArray(List inList) {
		String outVal = "";
		if (null != inList) {
			for (Iterator it = inList.iterator(); it.hasNext();) {
				Object next = it.next();
				outVal += (outVal.length() > 0 ? "," : "") + serializeToJS(next);
			}
		}

		return "[" + outVal + "]";
	}
	
	/**
	 * <p>Escapes the characters in a <code>String</code> using JavaScript String rules.</p>
	 * <p>Escapes any values it finds into their JavaScript String form.
	 * Deals correctly with quotes and control-chars (tab, backslash, cr, ff, etc.) </p>
	 * <p/>
	 * <p>So a tab becomes the characters <code>'\\'</code> and
	 * <code>'t'</code>.</p>
	 * <p/>
	 * <p>The only difference between Java strings and JavaScript strings
	 * is that in JavaScript, a single quote must be escaped.</p>
	 * <p/>
	 * <p>Example:
	 * <pre>
	 * input string: He didn't say, "Stop!"
	 * output string: He didn\'t say, \"Stop!\"
	 * </pre>
	 * </p>
	 *
	 * @param str String to escape values in, may be null
	 * @return String with escaped values, <code>null</code> if null string input
	 */
	public static String escapeJavaScript(String str) {
		if (str == null) {
			return null;
		}

		StringBuffer writer = new StringBuffer(str.length() * 2);
		int sz;
		sz = str.length();
		for (int i = 0; i < sz; i++) {
			char ch = str.charAt(i);

			// handle unicode
			if (ch > 0xfff) {
				writer.append("\\u" + hex(ch)); //$NON-NLS-1$
			} else if (ch > 0xff) {
				writer.append("\\u0" + hex(ch)); //$NON-NLS-1$
			} else if (ch > 0x7f) {
				writer.append("\\u00" + hex(ch)); //$NON-NLS-1$
			} else if (ch < 32) {
				switch (ch) {
					case '\b':
						writer.append('\\');
						writer.append('b');
						break;
					case '\n':
						writer.append('\\');
						writer.append('n');
						break;
					case '\t':
						writer.append('\\');
						writer.append('t');
						break;
					case '\f':
						writer.append('\\');
						writer.append('f');
						break;
					case '\r':
						writer.append('\\');
						writer.append('r');
						break;
					default:
						if (ch > 0xf) {
							writer.append("\\u00" + hex(ch)); //$NON-NLS-1$
						} else {
							writer.append("\\u000" + hex(ch)); //$NON-NLS-1$
						}
						break;
				}
			} else {
				switch (ch) {
					case '\'':
						// If we wanted to escape for Java strings then we would
						// not need this next line.
						// CD: causes invalid JSON
						//writer.append('\\'); 
						writer.append('\'');
						break;
					case '"':
						writer.append('\\');
						writer.append('"');
						break;
					case '\\':
						writer.append('\\');
						writer.append('\\');
						break;
					default:
						writer.append(ch);
						break;
				}
			}
		}
		return writer.toString();
	}

	/**
	 * <p>Returns an upper case hexadecimal <code>String</code> for the given
	 * character.</p>
	 *
	 * @param ch The character to convert.
	 * @return An upper case hexadecimal <code>String</code>
	 */
	private static String hex(char ch) {
		return Integer.toHexString(ch).toUpperCase();
	}

}
